﻿/****************************** Module Header ******************************\
* Module Name:    DTOHelper.cs
* Project:        ServiceCommon
* Copyright (c) NCSoft Corporation.
*
* Provide a way to convert ADO.NET Data objcet to relevant Entity(Model) object(s)
* by fast reflection. 
* 
* Performance Test:
* ==============================================================
* 
* Get Model
* 
* by Common Way:            7600ms. 
* by DTOHelper Way:         7750ms.
* 
* ==============================================================
* 
* Get Model List
* 
* by Common Way:            6781.3879 ms. 
* by DTOHelper Way:         6815.3898 ms. 
* 
* ==============================================================
* 
* Get GetDictionary
* 
* by Common Way:            6789.3884 ms. 
* by DTOHelper Way:         7671.4387 ms. 
*
* ==============================================================
* 
* History:
* * 8/19/2009 13:53 PM Lance Zhang Created
* * 8/24/2009 21:29 PM Lance Zhang Add Dictionary<K, T> GetDictionary<K, T>
* * 9/09/2009 21:29 PM Lance Zhang Add T New<T>()
* * X/XX/2009 XX:XX PM XXXX        Reviewed
\***************************************************************************/


using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Reflection;

namespace MM.Common
{
    public static class DTOHelper
    {
        #region Private Members

        private static Dictionary<Type, FastCreateInstanceHandler> TypeCache = new Dictionary<Type, FastCreateInstanceHandler>();
        private static Dictionary<PropertyInfo, FastPropertySetHandler> PropCache = new Dictionary<PropertyInfo, FastPropertySetHandler>();
        private static readonly object syncRoot = new object();

        #endregion

        /// <summary>
        /// Create entity(model) object list by IDataReader.
        /// </summary>
        /// <typeparam name="T">Type of Entity(Model)</typeparam>
        /// <param name="dr">Related datereader</param>
        /// <remarks>Please make sure the Fields Name of Entity(Model)
        /// and input daterader are same.</remarks>
        /// <returns>Object list of Entity(Model)</returns>
        public static List<T> CreateModelList<T>(IDataReader dr)
        {
            List<T> list = new List<T>();
            T obj;

            if (!TypeCache.Keys.Contains(typeof(T)))
            {
                lock (syncRoot)
                {
                    if (!TypeCache.Keys.Contains(typeof(T)))
                    {
                        TypeCache.Add(typeof(T), EmitHelper.GetInstanceCreator(typeof(T)));
                    }
                }
            }

            DataTable dcc = dr.GetSchemaTable();

            while (dr.Read())
            {
                obj = (T)TypeCache[typeof(T)].Invoke();

                foreach (PropertyInfo p in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance))
                {
                    if (!HasColumn(dcc, p.Name))
                        continue;

                    if (!PropCache.Keys.Contains(p))
                    {
                        lock (syncRoot)
                        {
                            if (!PropCache.Keys.Contains(p))
                            {
                                PropCache.Add(p, EmitHelper.GetPropertySetter(p));
                            }
                        }
                    }

                    if (p.PropertyType.IsEnum)
                    {
                        PropCache[p](obj, ChangeType(dr[p.Name], typeof(int)));
                    }
                    else
                        PropCache[p](obj, ChangeType(dr[p.Name], p.PropertyType));
                }

                list.Add(obj);

            }

            return list;
        }

        /// <summary>
        /// Create entity(model) object by IDataReader.
        /// </summary>
        /// <typeparam name="T">Type of Entity(Model)</typeparam>
        /// <param name="dr">Related datereader</param>
        /// <remarks>Please make sure the Fields Name of Entity(Model)
        /// and input daterader are same.</remarks>
        /// <returns>Object of Entity(Model)</returns>
        public static T CreateModel<T>(IDataReader dr)
        {

            if (!TypeCache.Keys.Contains(typeof(T)))
            {
                lock (syncRoot)
                {
                    if (!TypeCache.Keys.Contains(typeof(T)))
                    {
                        TypeCache.Add(typeof(T), EmitHelper.GetInstanceCreator(typeof(T)));
                    }
                }
            }

            T obj = (T)TypeCache[typeof(T)].Invoke();

            foreach (PropertyInfo p in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance))
            {
                if (!PropCache.Keys.Contains(p))
                {
                    lock (syncRoot)
                    {
                        if (!PropCache.Keys.Contains(p))
                        {
                            PropCache.Add(p, EmitHelper.GetPropertySetter(p));
                        }
                    }
                }
                PropCache[p](obj, ChangeType(dr[p.Name], p.PropertyType));
            }

            return obj;
        }

        /// <summary>
        /// Create entity(model) object Dictionary by IDataReader and given key name.
        /// </summary>
        /// <typeparam name="K">Type of Key field</typeparam>
        /// <typeparam name="T">Type of Entity(Model)</typeparam>
        /// <param name="keyName">name of key property</param>
        /// <param name="dr">Related datereader</param>
        /// <returns>Dictionary of models</returns>
        public static Dictionary<K, T> GetDictionary<K, T>(string keyName, IDataReader dr)
        {
            Dictionary<K, T> dic = new Dictionary<K, T>();

            T obj;

            if (!TypeCache.Keys.Contains(typeof(T)))
            {
                lock (syncRoot)
                {
                    if (!TypeCache.Keys.Contains(typeof(T)))
                    {
                        TypeCache.Add(typeof(T), EmitHelper.GetInstanceCreator(typeof(T)));
                    }
                }
            }

            object key = string.Empty;

            while (dr.Read())
            {
                obj = (T)TypeCache[typeof(T)].Invoke();

                foreach (PropertyInfo p in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance))
                {
                    if (!PropCache.Keys.Contains(p))
                    {
                        lock (syncRoot)
                        {
                            if (!PropCache.Keys.Contains(p))
                            {
                                PropCache.Add(p, EmitHelper.GetPropertySetter(p));
                            }
                        }
                    }

                    PropCache[p](obj, ChangeType(dr[p.Name], p.PropertyType));

                    if (p.Name == keyName)
                    {
                        key = EmitHelper.GetPropertyGetter(p).Invoke(obj);
                    }
                }

                dic.Add((K)key, obj);

            }

            return dic;
        }

        /// <summary>
        /// Create an instance of Generic type.
        /// </summary>
        /// <typeparam name="T">Generic type</typeparam>
        /// <returns>an new instance</returns>
        public static T New<T>()
        {
            if (!TypeCache.Keys.Contains(typeof(T)))
            {
                lock (syncRoot)
                {
                    if (!TypeCache.Keys.Contains(typeof(T)))
                    {
                        TypeCache.Add(typeof(T), EmitHelper.GetInstanceCreator(typeof(T)));
                    }
                }
            }

            T obj = (T)TypeCache[typeof(T)].Invoke();
            return obj;
        }

        private static bool HasColumn(DataTable dt, string columnName)
        {
            dt.DefaultView.RowFilter = "ColumnName= '" + columnName + "'";
            bool has = dt.DefaultView.Count > 0;
            dt.DefaultView.RowFilter = string.Empty;
            return has;
        }

        /// <summary>
        /// Returns an Object with the specified Type and whose value is equivalent to the specified object.
        /// </summary>
        /// <param name="value">An Object that implements the IConvertible interface.</param>
        /// <param name="conversionType">The Type to which value is to be converted.</param>
        /// <returns>An object whose Type is conversionType (or conversionType's underlying type if conversionType
        /// is Nullable&lt;&gt;) and whose value is equivalent to value. -or- a null reference, if value is a null
        /// reference and conversionType is not a value type.</returns>
        /// <remarks>
        /// This method exists as a workaround to System.Convert.ChangeType(Object, Type) which does not handle
        /// nullables as of version 2.0 (2.0.50727.42) of the .NET Framework. The idea is that this method will
        /// be deleted once Convert.ChangeType is updated in a future version of the .NET Framework to handle
        /// nullable types, so we want this to behave as closely to Convert.ChangeType as possible.
        /// This method was written by Peter Johnson at:
        /// http://aspalliance.com/author.aspx?uId=1026.
        /// </remarks>
        public static object ChangeType(object value, Type conversionType)
        {
            if (conversionType == null)
            {
                throw new ArgumentNullException("conversionType");
            }

            if (conversionType.IsGenericType &&
              conversionType.GetGenericTypeDefinition().Equals(typeof(Nullable<>)))
            {
                if (value == null || value is DBNull)
                {
                    return null;
                }

                System.ComponentModel.NullableConverter nullableConverter = new System.ComponentModel.NullableConverter(conversionType);
                conversionType = nullableConverter.UnderlyingType;
            }

            //if (conversionType.IsEnum)
            //{
            //    return Convert.ChangeType(value, typeof(int));
            //}
            return Convert.ChangeType(value, conversionType);
        }
    }
}
